package main

import (
	"archive/tar"
	"compress/gzip"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"math"
	"os"
	"path/filepath"
	"strconv"
	"time"

	"github.com/fatih/color"
)

type Config struct {
	Ignore  []string `json:"ignore"`
	Dir     string   `json:"dir"`
	TarFile string   `json:"tarFile"`
}

// 检测 是否需要过滤
func checkFilter(file string, isDir bool, filterStrs []string) bool {
	// 过滤 node_modules文件夹 且过滤 dist文件夹 数组写法
	for _, filterStr := range filterStrs {
		// 判断 fileterStr 是否有 *
		if !isDir && filterStr[len(filterStr)-1:] == "*" {
			// 过滤掉包含 *后的文件名
			if file[:len(filterStr)-1] == filterStr[:len(filterStr)-1] {
				return true
			}
		} else if file == filterStr {
			return true
		}
	}
	return false
}

// 备份压缩代码
func compress(src string, dst string, limit int, filterStrs []string) error {
	// 创建一个新的gzip文件
	gzFile, err := os.Create(dst)
	if err != nil {
		return err
	}
	defer gzFile.Close()

	// 创建一个新的gzip writer
	gzWriter := gzip.NewWriter(gzFile)
	defer gzWriter.Close()

	// 创建一个新的tar writer
	tarWriter := tar.NewWriter(gzWriter)
	defer tarWriter.Close()

	index := 0

	mu := math.Floor(float64(limit) / 100)

	var fileSize int64
	fileSize = 0

	srcLen := len(src)

	// 遍历源文件夹
	return filepath.Walk(src, func(file string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}

		isDir := info.IsDir()

		// 检测是否需要过滤
		checkFilterFlag := checkFilter(info.Name(), isDir, filterStrs)

		// 如果是文件夹则过滤文件夹 否则过滤文件
		if checkFilterFlag {
			if isDir {
				return filepath.SkipDir
			}
			return nil
		}

		if info.IsDir() {
			return nil
		}

		// 获取文件的header信息
		header, err := tar.FileInfoHeader(info, info.Name())
		if err != nil {
			return err
		}

		// 设置header的name为相对路径，以便保持目录结构
		header.Name = filepath.ToSlash(file)
		// 去除前置路径
		header.Name = header.Name[srcLen:]

		index++

		// 获取到当前大小
		fileSize = fileSize + header.Size
		if mu > 0 && index%int(mu) == 0 {
			color.Yellow("已压缩%dMB, 进度:%d%%", fileSize/1024/1024, int(math.Floor(float64(index)/mu)))
		}

		// 写入header信息
		if err := tarWriter.WriteHeader(header); err != nil {
			return err
		}

		// 如果是普通文件，写入文件内容
		if !info.IsDir() {
			data, err := os.Open(file)
			if err != nil {
				return err
			}
			defer data.Close()

			_, err = io.Copy(tarWriter, data)
			return err
		}

		return nil
	})
}

// 获取文件总数
func getLimit(dir string, filterStrs []string) (int, int) {
	// 打印进度条
	fileCount := 0
	fileSize := 0
	err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}

		isDir := info.IsDir()

		// 检测是否需要过滤
		checkFilterFlag := checkFilter(info.Name(), isDir, filterStrs)

		// 如果是文件夹则过滤文件夹 否则过滤文件
		if checkFilterFlag {
			if isDir {
				return filepath.SkipDir
			}
			return nil
		}

		if info.IsDir() {
			return nil
		}

		// 过滤掉目录，只计算文件
		if !info.IsDir() {
			fileSize = fileSize + int(info.Size())
			fileCount++
		}

		return nil
	})

	if err != nil {
		fmt.Println("Error:", err)
		return 0, 0
	}

	return fileCount, fileSize
}

// 读取配置
func getConfig() (*Config, error) {
	file, err := os.Open("config.json")
	if err != nil {
		fmt.Println(err)
		return nil, err
	}
	defer file.Close()

	bytes, err := ioutil.ReadAll(file)
	if err != nil {
		fmt.Println(err)
		return nil, err
	}

	var config Config
	if err := json.Unmarshal(bytes, &config); err != nil {
		fmt.Println(err)
		return nil, err
	}
	return &config, nil
}

func main() {
	config, err := getConfig()
	if err != nil {
		color.Red("JSON配置文件不存在或json格式错误 5S后自动退出")
		time.Sleep(5 * time.Second)
		panic(err)
	}

	absPath, err := filepath.Abs(config.Dir)
	if err != nil {
		panic(err)
	}
	config.Dir = absPath

	fmt.Println("配置文件读取成功，备份目录:" + config.Dir)
	// 遍历 过滤文件
	fmt.Println("过滤文件夹如下:")
	for _, ignore := range config.Ignore {
		fmt.Println("\t" + ignore)
	}

	fmt.Println("开始压缩备份- 计算总文件数和大小 请稍等...")
	limit, size := getLimit(config.Dir, config.Ignore)
	sizeStr := strconv.Itoa(size/1024) + "KB"
	if size > 1024*1024 {
		sizeStr = strconv.Itoa(size/1024/1024) + "MB"
	}
	fmt.Println(" 总文件数:" + strconv.Itoa(limit) + " 总文件大小:" + sizeStr)

	if limit == 0 {
		color.Red("备份目录为空")
		time.Sleep(5 * time.Second)
		panic("备份目录为空")
	}

	if err := compress(config.Dir, config.TarFile, limit, config.Ignore); err != nil {
		color.Red("文件是否被占用 压缩失败 5S后自动退出")
		time.Sleep(5 * time.Second)
		panic(err)
	}

	// 求压缩后文件大小
	fileInfo, err := os.Stat(config.TarFile)
	if err != nil {
		color.Red("没找到压缩文件 5S后自动退出")
		panic(err)
	}
	tarSize := fileInfo.Size()
	var tarSizeStr string
	if tarSize < 1024 {
		tarSizeStr = fmt.Sprintf("%d B", tarSize)
	} else if tarSize < 1024*1024 {
		tarSizeStr = fmt.Sprintf("%.2f KB", float64(tarSize)/1024)
	} else {
		tarSizeStr = fmt.Sprintf("%.2f MB", float64(tarSize)/(1024*1024))
	}

	color.Green("压缩成功 " + config.TarFile + " " + tarSizeStr + " 5秒后自动退出")
	time.Sleep(5 * time.Second)
}
